<?php

/*
 * Copyright (c) 2011 Antoine d'Otreppe de Bouvette
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 */

abstract class BaseDao {

    /**
     * The database object
     * @var Database
     */
    private $database;

    /**
     * The table name for this entity
     * @var string
     */
    private $table;

    /**
     * @param string $table the table name in database
     */
    public function __construct($table) {
        $this->table = $table;
    }

    public function setDatabase(Database $database) {
        $this->database = $database;
    }

    /**
     * @return Database
     */
    public function getDatabase() {
        return $this->database;
    }

    /**
     * Finds entities matching the filter.
     * @param array $filter an associative array representing the columns
     * to be matched and their expected values 
     */
    protected function find(array $filter, array $customFilter = array(), $projection = array('*')) {
        $args = array();
        $statement = sprintf('SELECT %s FROM #%s', implode(', ', $projection), $this->table);

        $statement .= $this->makeWhereClause($filter, $customFilter, $args);
        return $this->executeSelect($statement, $args);
    }

    protected function findWithLimit(array $filter, array $customFilter, array $projection, $max, $offset = null) {
        $args = array();
        $limit = $max;
        if ($offset !== null) {
            $limit .= ' OFFSET ' . $offset;
        }

        $statement = sprintf('SELECT %s FROM #%s', implode(', ', $projection), $this->table);
        $statement .= $this->makeWhereClause($filter, $customFilter, $args);
        $statement .= sprintf(' LIMIT %s', $limit);

        return $this->executeSelect($statement, $args);
    }

    private function executeSelect($statement, $args) {
        $ps = $this->database->vquery($statement, $args);
        // TODO Use a fetchable
        return new PDOIterator($ps, array($this, 'convertToPhp'));
    }

    protected function insert(array $values, $returnId = false, $sequence = null) {
        $statement = sprintf('INSERT INTO #%s (%s) VALUES (%s)', $this->table, implode(', ', array_keys($values)), implode(', ', array_fill(0, count($values), '?')));
        $this->database->vquery($statement, array_values($values));

        if ($returnId) {
            return $this->database->lastInsertId($sequence);
        }
    }

    protected function update(array $values, array $filter) {
        $args = array();
        $columns = array();

        foreach ($values as $column => $value) {
            $columns[] = $column . ' = ?';
            $args[] = $value;
        }

        $statement = sprintf('UPDATE #%s SET %s', $this->table, implode(', ', $columns));
        $statement .= $this->makeWhereClause($filter, array(), $args);

        $ps = $this->database->vquery($statement, $args);
        return $ps->rowCount();
    }

    protected function delete($filter) {
        $args = array();
        $statement = sprintf('DELETE FROM #%s', $this->table);
        $statement .= $this->makeWhereClause($filter, array(), $args);

        $ps = $this->database->vquery($statement, $args);
        return $ps->rowCount();
    }

    private function makeWhereClause(array $filter, array $custom, array &$args) {
        if (!(empty($filter) && empty($custom))) {
            $tests = array();
            foreach ($filter as $column => $value) {
                $tests[] = $column . ' = ?';
                $args[] = $value;
            }
            $tests = array_merge($tests, $custom);

            return ' WHERE ' . implode(' AND ', $tests);
        }
    }
    
    public function convertToPhp(array $row, $into=null) {
        return $row;
    }
    
    /**
     * Format a DateTime object to a standard SQL DATE
     * 
     * @param DateTime $date
     * @return string
     */
    public function dateToSql(DateTime $date=null) {
        return $this->maybeFormatToSql('Y-m-d', $date);
    }
    
    /**
     * Format a DateTime object to a standard SQL DATETIME
     *  
     * @param DateTime $dateTime
     * @return string
     */
    public function dateTimeToSql(DateTime $dateTime=null) {
        return $this->maybeFormatToSql('Y-m-d H:i:s', $dateTime);
    }
    
    /**
     * Format a DateTime object to a standard SQL TIME
     * 
     * @param DateTime $time
     * @return string
     */
    public function timeToSql(DateTime $time=null) {
        return $this->maybeFormatToSql('H:i:s', $time);
    }
    
    /**
     * @param string $format
     * @param DateTime $dateTime
     * @return string
     */
    private function maybeFormatToSql($format, DateTime $dateTime=null) {
        if ($dateTime !== null) {
            return $dateTime->format($format);
        }
        else {
            return null;
        }
    }
    
    /**
     * Parse a standard SQL DATETIME to a DateTime object
     * @param string $time
     * @param DateTimeZone $timezone
     * @return DateTime
     */
    public function sqlToDateTime($time, DateTimeZone $timezone=null) {
        if ($time !== null) {
            return $this->maybeParseSql('Y-m-d H:i:s', $time, $timezone);
        }
    }
    
    
    /**
     * Parse a standard SQL DATE to a DateTime object
     * @param string $time
     * @param DateTimeZone $timezone
     * @return DateTime
     */
    public function sqlToDate($time, DateTimeZone $timezone=null) {
        return $this->maybeParseSql('Y-m-d', $time, $timezone);
    }
    
    /**
     * Parse a standard SQL TIME to a DateTime object
     * @param string $time
     * @param DateTimeZone $timezone
     * @return DateTime
     */
    public function sqlToTime($time, DateTimeZone $timezone=null) {
        return $this->maybeParseSql('H:i:s', $time, $timezone);
    }
    
    private function maybeParseSql($format, $time, DateTimeZone $tz=null) {
        if ($time !== null) {
            return DateTime::createFromFormat($format, $time, $tz);
        }
        else {
            return null;
        }
    }
    
    /**
     * Inject values into an object, converting property names
     * 
     * @param object $object
     * @param array $values 
     */
    public function inject($object, array $values) {
        foreach ($values as $property=>$value) {
            $property = $this->getPhpName($property);
            $object->{'set'.ucfirst($property)}($value);
        }
    }

    /**
     * Extract properties from an object, adapting names to SQL
     * 
     * @param object $object
     * @param array $properties
     * @return array
     */
    public function extract($object, array $properties) {
        $result = array();

        foreach ($properties as $property) {
            $result[$this->getSqlName($property)] =
                    $object->{'get'.ucfirst($property)}();
        }

        return $result;
    }
    
    /**
     * PHP'ize a SQL field name. E.g. "account_name" becomes "accountName"
     * 
     * @param string $sqlName 
     * @return string
     */
    private function getPhpName($sqlName) {
        return preg_replace_callback('/_([a-z])/', function ($match) {
            return strtoupper($match[1]);
        }, $sqlName);
    }
    
    
    /**
     * SQL'ize a PHP property name. E.g. "accountName" becomes "account_name"
     * 
     * @param string $phpName 
     * @return string
     */
    private function getSqlName($phpName) {
        return preg_replace_callback('/([A-Z])/', function ($match) {
            return '_' . strtolower($match[1]);
        }, $phpName);
    }
}
